﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using RayDen.Library.Core.Collections;
using RayDen.Library.Core.Primitives;
using RayDen.Library.Entity;
using RayDen.Library.Entity.Scene;

namespace RayDen.Library.Data.Import.OBJLoader
{
    using Vector3 = Vector;
    public struct Int3
    {
        public int v0, v1, v2;

        public Int3(int i0, int i1, int i2)
        {
            v0 = i0;
            v1 = i1;
            v2 = i2;
        }
    }

    public class ObjGroupInfo
    {
        public string name;
        public string mtrl;
        public List<Int3> VIndices;
        public List<Int3> NIndices;
        public List<Int3> TIndices;

        public ObjGroupInfo(string name)
        {
            this.name = name;
            mtrl = null;
            VIndices = new List<Int3>();
            NIndices = new List<Int3>();
            TIndices = new List<Int3>();
        }
    }


    public struct ObjFileMaterial
    {
        public float Ns;
        public float Ni;
        public float d;
        public float Tr;
        public float illum;

        public Vector Tf;
        public Vector Ka;
        public Vector Kd;
        public Vector Ks;
        public Vector Ke;

        public string Map_Ka { get; set; }
        public string Map_Kd { get; set; }
        public string Map_Ks { get; set; }
        public string Map_d { get; set; }
        public float Bump { get; set; }

        public void SetMaps(string ka = null, string kd = null, string ks = null, string mapd = null)
        {
            this.Map_Ks = ks ?? this.Map_Ks;
            this.Map_Kd = kd ?? Map_Kd;
            this.Map_Ka = ka ?? Map_Ka;
            this.Map_d = mapd ?? Map_d;

        }
    }


    [EntityAction(ActionName = "LoadObj")]
    public class ObjLoader : IGeometryLoader
    {


        public float Scale = 1.0f;

        public bool ParseMaterials = true;
        public bool ParseNormals = true;
        public bool GenerateNormals = true;
        public bool GenerateGeometry = true;
        public bool FrongFaceWinding = true;

        public List<Vector> Positions;
        public List<Vector> Normals;
        public List<Vector> Texcoords;

        protected Dictionary<string, ObjFileMaterial> mMtrls;
        protected string mCurrentMtrlName;
        protected ObjFileMaterial mCurrentMtrl;

        protected ObjGroupInfo mCurrentGroup = null;
        protected string mCurrentGroupName;
        public List<ObjGroupInfo> Groups;

        protected string mDirectory;


        public SceneGeometryInfo Load(string filename, bool invertNormals = false, params string[] additionalFiles)
        {
            var FileName = filename;

            if (!File.Exists(filename))
            {
                throw new FileNotFoundException("OBJLoader Error: File not found", filename);
            }

            Positions = new List<Vector>();
            Normals = new List<Vector>();
            Texcoords = new List<Vector>();

            

            mMtrls = new Dictionary<string, ObjFileMaterial>();
            Groups = new List<ObjGroupInfo>();

            mCurrentGroup = null;
            mCurrentMtrlName = null;
            mCurrentMtrl = default(ObjFileMaterial);

            Console.Write("Loading: " + Path.GetFileName(FileName) + "... ");
            //float start = Time.GetTimeInSecs();

            mDirectory = Path.GetDirectoryName(FileName) + "\\";

            mCurrentGroup = new ObjGroupInfo("default");
            Groups.Add(mCurrentGroup);

            ParseFile(ParseObjToken, FileName);
            CreateNormals();
            SceneGeometryInfo result = new SceneGeometryInfo();
            result.Vertices = this.Positions.Select(i => (Point) i).ToArray();
            result.Normals = this.Normals.ToArray();
            result.TexCoords = this.Texcoords.ToArray();
            var geo = new List<GeometryInfo>();

            foreach (var objGroup in this.Groups)
            {
                if (!objGroup.VIndices.Any())
                {
                    Console.WriteLine("No inidexz-");
                    continue;
                    
                }

                if (string.IsNullOrWhiteSpace(objGroup.mtrl))
                {
                    Debugger.Break();
                }

                geo.Add(new GeometryInfo()
                {
                    IndexData = new IntStore(objGroup.VIndices.SelectMany(i=> new[] {i.v0, i.v1, i.v2})),
                    NormalIndexData = new IntStore(objGroup.NIndices.SelectMany(i=> new[] {i.v0, i.v1, i.v2})),
                    TextureIndexData = new IntStore(objGroup.TIndices.SelectMany(i => new[] { i.v0, i.v1, i.v2 })),
                    Name = objGroup.name,
                    MaterialName = objGroup.mtrl,
                });                
            }
            result.Geometry = geo.ToArray();

            return result;
        }


        protected static char[] omitchars = new char[] { ' ', '\r', '\n', '\t', '/' };
        protected void ParseFile(MaterialLoader.ParseHandler parser, string filename)
        {
            StreamReader reader = new StreamReader(filename);

            while (!reader.EndOfStream)
            {
                string line = reader.ReadLine();

                if (string.IsNullOrEmpty(line))
                    continue;

                string[] tokens = line.Split(omitchars, StringSplitOptions.RemoveEmptyEntries);

                if (tokens == null || tokens[0].StartsWith("#"))
                    continue;

                parser(line, tokens);
            }
        }
        protected void CreateNormals()
        {
            //only allow normal generation for objs that don't have normals
            if (Normals.Count > 0 || !GenerateNormals)
            {
                GenerateNormals = false;
                return;
            }

            float winding = 1.0f;
            if (FrongFaceWinding == false)
                winding = -1.0f;

            //Console.Write("[Creating normals: ");
            //float start = Time.GetTimeInSecs();

            //fill with zeros
            Vector3 zero = new Vector3(0, 0, 0);
            for (int i = 0; i < Positions.Count; i++)
                Normals.Add(zero);

            //loop through the triangles
            foreach (var group in Groups)
            {
                for (int i = 0; i < group.VIndices.Count; i++)
                {
                    Int3 index = group.VIndices[i];
                    Vector3 v0 = Positions[index.v0];
                    Vector3 v1 = Positions[index.v1];
                    Vector3 v2 = Positions[index.v2];

                    Vector3 leg0 = v1 - v0;
                    Vector3 leg1 = v2 - v0;
                    Vector3 normal = winding * Vector3.Cross(ref leg0, ref leg1);

                    Normals[index.v0] += normal;
                    Normals[index.v1] += normal;
                    Normals[index.v2] += normal;
                }
            }

            for (int i = 0; i < Positions.Count; i++)
            {
                Normals[i].Normalize();
            }

            //Console.Write("{0:0.00}s] ", Time.GetTimeInSecs() - start);
        }

        private void ParseObjToken(string line, string[] tokens)
        {
            string token = tokens[0].ToLower();
            try
            {
                switch (token)
                {
                    case "v":
                        {
                            float x = float.Parse(tokens[1], System.Globalization.CultureInfo.InvariantCulture);
                            float y = float.Parse(tokens[2], System.Globalization.CultureInfo.InvariantCulture);
                            float z = float.Parse(tokens[3], System.Globalization.CultureInfo.InvariantCulture);

                            Vector3 p = new Vector3(x * Scale, y * Scale, z * Scale);
                            //BBox.AddFloat3(ref p);

                            Positions.Add(p);
                            break;
                        }
                    case "vn":
                        {
                            if (ParseNormals)
                            {
                                float x = float.Parse(tokens[1], System.Globalization.CultureInfo.InvariantCulture);
                                float y = float.Parse(tokens[2], System.Globalization.CultureInfo.InvariantCulture);
                                float z = float.Parse(tokens[3], System.Globalization.CultureInfo.InvariantCulture);

                                float modifier = -1.0f;
                                Normals.Add(new Vector3(x, y, z) * modifier);
                            }
                            break;
                        }
                    case "vt":
                        {
                            float x = float.Parse(tokens[1], System.Globalization.CultureInfo.InvariantCulture);
                            float y = float.Parse(tokens[2], System.Globalization.CultureInfo.InvariantCulture);

                            Texcoords.Add(new Vector(x, y, 0f));
                            break;
                        }
                    case "f":
                        {
                            int v0 = 0, v1 = 0, v2 = 0;

                            if (tokens.Length >= 10)
                            {
                                //full v, vn, vt indices
                                v0 = int.Parse(tokens[1], System.Globalization.CultureInfo.InvariantCulture);
                                v1 = int.Parse(tokens[4], System.Globalization.CultureInfo.InvariantCulture);
                                v2 = int.Parse(tokens[7], System.Globalization.CultureInfo.InvariantCulture);

                                mCurrentGroup.VIndices.Add(new Int3(v0 - 1, v1 - 1, v2 - 1));

                                v0 = int.Parse(tokens[2], System.Globalization.CultureInfo.InvariantCulture);
                                v1 = int.Parse(tokens[5], System.Globalization.CultureInfo.InvariantCulture);
                                v2 = int.Parse(tokens[8], System.Globalization.CultureInfo.InvariantCulture);
                                mCurrentGroup.TIndices.Add(new Int3(v0 - 1, v1 - 1, v2 - 1));

                                if (ParseNormals)
                                {
                                    v0 = int.Parse(tokens[3], System.Globalization.CultureInfo.InvariantCulture);
                                    v1 = int.Parse(tokens[6], System.Globalization.CultureInfo.InvariantCulture);
                                    v2 = int.Parse(tokens[9], System.Globalization.CultureInfo.InvariantCulture);
                                    mCurrentGroup.NIndices.Add(new Int3(v0 - 1, v1 - 1, v2 - 1));
                                }

                            }
                            else if (tokens.Length == 7)
                            {
                                //v, ( vn | vt ) indices
                                v0 = int.Parse(tokens[1], System.Globalization.CultureInfo.InvariantCulture);
                                v1 = int.Parse(tokens[3], System.Globalization.CultureInfo.InvariantCulture);
                                v2 = int.Parse(tokens[5], System.Globalization.CultureInfo.InvariantCulture);

                                mCurrentGroup.VIndices.Add(new Int3(v0 - 1, v1 - 1, v2 - 1));

                                v0 = int.Parse(tokens[2], System.Globalization.CultureInfo.InvariantCulture);
                                v1 = int.Parse(tokens[4], System.Globalization.CultureInfo.InvariantCulture);
                                v2 = int.Parse(tokens[6], System.Globalization.CultureInfo.InvariantCulture);

                                if (line.Contains("//"))
                                {
                                    if (ParseNormals)
                                        mCurrentGroup.NIndices.Add(new Int3(v0 - 1, v1 - 1, v2 - 1));
                                }
                                else
                                {
                                    mCurrentGroup.TIndices.Add(new Int3(v0 - 1, v1 - 1, v2 - 1));
                                }
                            }
                            else if (tokens.Length == 4)
                            {
                                //v indices
                                v0 = int.Parse(tokens[1], System.Globalization.CultureInfo.InvariantCulture);
                                v1 = int.Parse(tokens[2], System.Globalization.CultureInfo.InvariantCulture);
                                v2 = int.Parse(tokens[3], System.Globalization.CultureInfo.InvariantCulture);

                                mCurrentGroup.VIndices.Add(new Int3(v0 - 1, v1 - 1, v2 - 1));
                            }
                            else
                            {
                                throw new Exception("Error parsing obj file");
                            }

                            break;
                        }
                    case "g":
                        {
                            string name = tokens[1].ToLowerInvariant();
                            mCurrentGroupName = name;

                            mCurrentGroup = new ObjGroupInfo(name) { mtrl =  mCurrentMtrlName };
                            Groups.Add(mCurrentGroup);
                            break;
                        }
                    case "usemtl":
                        {
                            string name = tokens[1].ToLowerInvariant();

                            string groupname = mCurrentGroupName.ToLowerInvariant();

                            mCurrentGroup = new ObjGroupInfo(groupname);
                            Groups.Add(mCurrentGroup);

                            mCurrentGroup.mtrl = name;
                            mCurrentMtrlName = name;
                            break;
                        }
                    case "mtllib":
                        {
                            mCurrentMtrl = default(ObjFileMaterial);

                            string name = mDirectory + tokens[1];
                            ParseFile(ParseMtrlToken, name);

                            if (mCurrentMtrlName != null)
                            {
                                mMtrls.Add(mCurrentMtrlName, mCurrentMtrl);
                            }
                            mCurrentMtrlName = null;
                            break;
                        }
                    default:
                        break;
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine("Error parsing obj token: " + ex.Message);
            }
        }

        private void ParseMtrlToken(string line, string[] tokens)
        {
            string token = tokens[0].ToLower();
            try
            {
                switch (token)
                {
                    case "newmtl":
                        {
                            if (mCurrentMtrlName != null)
                            {
                                mMtrls.Add(mCurrentMtrlName, mCurrentMtrl);
                            }

                            mCurrentMtrlName = tokens[1];
                            mCurrentMtrl = new ObjFileMaterial();
                            break;
                        }
                    case "ns":
                        {
                            mCurrentMtrl.Ns = float.Parse(tokens[1], CultureInfo.InvariantCulture);
                            break;
                        }
                    case "ni":
                        {
                            mCurrentMtrl.Ni = float.Parse(tokens[1], CultureInfo.InvariantCulture);
                            break;
                        }
                    case "d":
                        {
                            mCurrentMtrl.d = float.Parse(tokens[1], CultureInfo.InvariantCulture);
                            break;
                        }
                    case "tr":
                        {
                            mCurrentMtrl.Tr = float.Parse(tokens[1], CultureInfo.InvariantCulture);
                            break;
                        }
                    case "illum":
                        {
                            mCurrentMtrl.illum = float.Parse(tokens[1], CultureInfo.InvariantCulture);
                            break;
                        }
                    case "tf":
                        {
                            float x = float.Parse(tokens[1], CultureInfo.InvariantCulture);
                            float y = float.Parse(tokens[2], CultureInfo.InvariantCulture);
                            float z = float.Parse(tokens[3], CultureInfo.InvariantCulture);
                            mCurrentMtrl.Tf = new Vector(x, y, z);
                            break;
                        }
                    case "ka":
                        {
                            float x = float.Parse(tokens[1], CultureInfo.InvariantCulture);
                            float y = float.Parse(tokens[2], CultureInfo.InvariantCulture);
                            float z = float.Parse(tokens[3], CultureInfo.InvariantCulture);
                            mCurrentMtrl.Ka = new Vector(x, y, z);
                            break;
                        }
                    case "kd":
                        {
                            float x = float.Parse(tokens[1], CultureInfo.InvariantCulture);
                            float y = float.Parse(tokens[2], CultureInfo.InvariantCulture);
                            float z = float.Parse(tokens[3], CultureInfo.InvariantCulture);
                            mCurrentMtrl.Kd = new Vector(x, y, z);
                            break;
                        }
                    case "ks":
                        {
                            float x = float.Parse(tokens[1], CultureInfo.InvariantCulture);
                            float y = float.Parse(tokens[2], CultureInfo.InvariantCulture);
                            float z = float.Parse(tokens[3], CultureInfo.InvariantCulture);
                            mCurrentMtrl.Ks = new Vector(x, y, z);
                            break;
                        }
                    case "ke":
                        {
                            float x = float.Parse(tokens[1], CultureInfo.InvariantCulture);
                            float y = float.Parse(tokens[2], CultureInfo.InvariantCulture);
                            float z = float.Parse(tokens[3], CultureInfo.InvariantCulture);
                            mCurrentMtrl.Ke = new Vector(x, y, z);
                            break;
                        }
                    case "map_ka":
                        {
                            //mCurrentMtrl.Map_Ka = tokens.Length < 2 ? tokens.Last() : line.Substring(line.IndexOf(tokens[1]));
                            mCurrentMtrl.SetMaps(ka: tokens.Length < 2 ? tokens.Last() : line.Substring(line.IndexOf(tokens[1])));
                            break;
                        }
                    case "map_kd":
                        {
                            //mCurrentMtrl.Map_Kd = tokens.Length < 2 ? tokens.Last() : line.Substring(line.IndexOf(tokens[1]));
                            mCurrentMtrl.SetMaps(kd: tokens.Length < 2 ? tokens.Last() : line.Substring(line.IndexOf(tokens[1])));
                            break;
                        }
                    case "map_ks":
                        {
                            mCurrentMtrl.SetMaps(ks: tokens.Length < 2 ? tokens.Last() : line.Substring(line.IndexOf(tokens[1])));
                            //Map_Ks = line.Substring(line.IndexOf(tokens[1]));
                            break;
                        }
                    case "map_refl":
                        {
                            mCurrentMtrl.SetMaps(ks: tokens.Length < 2 ? tokens.Last() : line.Substring(line.IndexOf(tokens[1])));
                            //Map_Ks = line.Substring(line.IndexOf(tokens[1]));
                            break;
                        }
                    case "bump":
                        {
                            mCurrentMtrl.SetMaps(mapd: tokens.Length < 2 ? tokens.Last() : line.Substring(line.IndexOf(tokens[1])));

                            //mCurrentMtrl.Map_d = line.Substring(line.IndexOf(tokens[1]));
                            break;
                        }
                    default:
                        break;
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine("Error parsing mtrl token: " + ex.Message);
            }
        }


    }
}
